三天时间,从一个 10 行的 API 调用,到一个具备自我审查与纠正能力的模块化 Agent 系统。这篇文章记录了完整的学习路径、踩过的每一个坑、以及由此提炼出的工程认知。
作者: Jinkun
时间: 2026年4月
项目地址: github.com/yaoziyaoguai/my-first-agent
技术栈: Python 3.12 · Anthropic SDK(兼容协议)· Kimi-k2.5 · Qwen3-max
一、为什么写这篇文章
随着 AI Agent 从“能跑”走向“能稳定落地”,工程实践中逐渐形成了两条重要的方法论脉络:其一是 Context Engineering(参考 ),关注如何为模型组织、筛选与注入高质量上下文;其二是 Harness Engineering,关注如何通过外部工程结构来约束、验证并增强 Agent 的行为可靠性。前者并非在 2026 年才出现,而是在 2025 年已被持续推广,并由 Anthropic 进一步系统化;后者则在 2026 年由 ThoughtWorks 的 Birgitta Böckeler 在 Martin Fowler 网站上得到更完整的阐述。 读完这些文章后我有一个强烈的感觉:光看概念远远不够,必须动手构建才能真正理解。于是我决定从零开始,不用任何 Agent 框架,只用 Python + Anthropic SDK,一步一步构建一个完整的 Agent,在过程中体会这两个框架的每一个设计决策。
二、核心概念:先搞清楚我们在造什么
2.1 Agent 的本质
在动手之前,需要先破除一个误解:Agent 不是一个更聪明的聊天机器人。
大模型(LLM)本身是完全无状态的。每次调用都是一次全新的推理——它不记得你是谁,不记得上一轮说了什么。所谓的"记忆"完全是外部代码维护的一个消息列表(messages),每次调用时完整发送给模型,模型读一遍,产生回复,然后就忘了。
同样,模型也从未"真正"调用过任何工具。它只是生成了一段结构化文本说"我想调用 calculate,参数是 123 * 456",然后就停了。是我们写的代码在中间做了所有实际工作——解析请求、执行计算、把结果包装成特定格式塞回去。模型下次被调用时看到结果,它的训练让它"理解"这个对话模式。
这意味着 Agent 的"自主性"是两层配合的结果:模型提供决策能力,外部代码提供循环机制。 缺了任何一层都不行。
Agent 的核心公式:
Agent = Model + Harness
2.2 Context Engineering:管模型"看到"什么
Anthropic 的定义:优化送入 LLM 的 token 的效用,在模型固有的约束下,持续达成预期结果。
简单说就是:在每一个决策时刻,应该把哪些信息放进模型的上下文窗口?
它面对三个经典挑战:
- Lost in the Middle:上下文过长时,模型对中间部分的注意力下降,倾向于关注开头和结尾
- 信息缺失:关键信息未被送入上下文,模型只能猜测,产生幻觉
- 窗口溢出:历史累积超出容量,早期信息被丢失
2.3 Harness Engineering:让 Agent 可靠运行
Harness 是"模型之外的一切"。它的控制体系有两个维度:
按方向分:
- Guides(前馈控制):行动之前预防问题
- Sensors(反馈控制):行动之后检测并纠正
按材质分:
- Computational(计算型):确定性的、快速的(如白名单、类型检查、Linter)
- Inferential(推理型):非确定性的、基于 LLM 的(如 AI Code Review)
Harness 要调节三个维度:可维护性(工具最成熟)、架构适应性(中等成熟)、行为正确性(最难,原文称之为"房间里的大象")。
2.4 两者的关系
这个问题我纠结了很久。一开始我以为是包含关系——Context Engineering 被 Harness Engineering 包含。后来反复讨论,发现更准确的理解是:
Context Engineering 是底层管道——负责把信息送进模型。 Harness Engineering 是上层策略——用这个管道来构建完整的控制体系。
同一个组件往往同时扮演两个角色。比如 CLAUDE.md 文件,从 Context Engineering 角度看是"送进模型的信息",从 Harness 角度看是"一个前馈控制器"。在实践中它们交织在一起,不需要强行划分边界。
三、渐进式构建:从 10 行到 800 行
3.1 演进路径总览
| 阶段 | 文件 | 核心突破 | 代码量 |
|---|---|---|---|
| 1 | 01_single_call.py | API 能通了 | ~10 行 |
| 2 | 02_agent_loop.py | 有记忆了(messages 列表) | ~30 行 |
| 3 | 03_agent_with_tool.py | 能动手了(工具调用 + Agent Loop) | ~80 行 |
| 4 | 04_agent_with_harness.py | 有约束了(日志 + 确认 + 白名单) | ~150 行 |
| 5 | 05_agent_with_files.py | 完整系统 | ~800 行 |
| 6 | 模块化重构 | 拆分为 agent/ 包 | ~800 行(7 个文件) |
3.2 阶段一:从沉默到说话
第一个文件只做一件事——调用 API,确认能通。
response = client.messages.create(
model="kimi-k2.5",
max_tokens=1024,
messages=[
{"role": "user", "content": "你好,请用一句话介绍你自己"}
],
)
print(response.content[0].text)
第一个坑就来了:模型返回的第一个 content block 不是文字,而是一个 ThinkingBlock(Kimi 的深度思考模式)。这让我学到了第一课:不同模型的返回格式可能不同,不能假设 content[0] 就是文字。
3.3 阶段二:从失忆到记忆
第二版加了一个 messages 列表和一个 while True 循环。每轮对话的用户输入和模型回复都追加到列表里,下次调用时完整发送。
关键认知:这个 messages 列表就是 Context Engineering 的核心对象。 后面所有的上下文管理——压缩、截断、摘要——都是在操作这个列表。
3.4 阶段三:从说话到动手
给 Agent 加了一个计算器工具。这一步的概念跨越最大——模型的回复不再只是文字,它可能是一个"我想调用工具"的请求。代码需要检测这个请求、执行工具、把结果塞回去、再次调用模型。
此时代码里出现了两个循环:外层循环处理用户交互,内层循环处理工具调用。内层循环就是真正的 Agent Loop。
一个深刻的感悟来自工具描述的实验:如果把计算器的 description 从"计算数学表达式"改成"翻译英文",模型就不会在用户问数学题时调用它。工具描述是模型理解工具的唯一依据。
3.5 阶段四:从裸奔到穿甲
这一步加了三个 Harness 机制: 这三个机制的选择并非来自某个特定的论文或框架,而是基于三个基本工程原则:可观测性(你无法改进你看不见的东西)、最小权限(不在白名单里的一律拒绝)、以及人在回路(Human-in-the-loop,关键决策保留人类判断)。后续的所有 Harness 机制——权限分级、源码保护、跨模型审查——都不是预先设计的,而是在实际使用中遇到问题后逐步"长出来"的。这正好印证了 Harness Engineering 的核心理念:Steering Loop 是一个持续迭代的过程。
可观测性 → 日志(你得看见发生了什么)
可控制性 → 确认(你得能拦住危险操作)
可预测性 → 白名单(你得限定行为边界)
日志系统(Sensor,计算型)——记录每一步决策。后来的每一次调试都依赖这个日志。教训:你无法改进你看不见的东西。
执行确认(Guide,计算型)——工具执行前让人类确认。当时只有计算器觉得多余,后来加了文件写入才体会到它的救命价值。
工具白名单(Guide,计算型)——防止模型幻觉出不存在的工具。
还加了 Session 快照功能,遇到了第一个工程问题:Anthropic SDK 返回的是 Python 对象,不是普通字典,json.dump 无法序列化。解决方法是写了一个 make_serializable 函数递归转换。Agent 跟外部世界交互时,数据格式的转换是常见的工程细节。
3.6 阶段五:完整系统的诞生(与无数的坑)
这个阶段是密度最高的,几乎每一步都踩出了新问题。
权限分级的设计
给 Agent 加了文件读写能力后,一个核心问题浮出水面:读文件和写文件的危险等级不同,应该施加不同的控制。
我设计的分级规则:
- 读项目内文件 → 静默执行
- 读项目外文件 → 需确认
- 写任何文件 → 需确认
设计原则是:控制强度与操作风险成正比。 风险由两个因素决定——可逆性和影响范围。
上下文膨胀的三次搏斗
第一次:按条数压缩。 当 messages 超过 10 条时,把旧消息用 LLM 总结成摘要。结果发现压缩后 Agent 跑偏了——关键信息(文件名、路径)被摘要丢掉了。
第二次:压缩前先截断 tool_result。 在让 LLM 总结之前,先把 tool_result 里的大块内容截断成摘要级别。两阶段策略:"先降噪,再总结"。效果好了很多。
第三次:按字节数触发。 Agent 读了一个大文件,只有 5 条消息但总内容量巨大,直接触发了 API 的 Backend buffer overflow。原来只按条数判断是不够的,加了字节数作为第二个触发条件。
源码保护的攻防
Agent 在被要求重构代码时,试图修改自己的 .py 源文件。这是一个 Harness 问题——Agent 不应该修改自己的代码。 加了源码保护后,又发现太严格了——Agent 想在 workspace 下创建新的 .py 文件也被拦住了。最终的规则是:只保护已存在的 .py 文件,新文件可以创建但需要确认。
流式输出的体验优化
messages.create() 会等模型把整个回复生成完才返回,十几秒的等待中什么都看不到。改用 messages.stream() 后文字一个字一个字出现。但 tool_use 阶段没有文字输出,用户又觉得卡了——于是加了"🔧 正在规划工具调用..."的提示。
流式输出不是 Context Engineering(没改变模型看到的信息),而是 Harness 的一部分——实时可见性本身就是一种 Sensor。
最痛苦的 Bug:缩进导致的逻辑错误
在处理工具执行结果的代码中,else 分支对齐到了 if tool_name == "write_file" 而非 if approved。结果:所有非写文件的工具(包括 read_file)在执行成功后,返回值被覆盖为"用户拒绝了此操作"。
模型收到这个假的拒绝信息后,以为读取被拒绝了,反复用不同路径重试。我花了很长时间才定位到这个问题——因为日志里记录的是真实的执行结果(成功),而模型看到的是被覆盖后的结果(拒绝)。Python 的缩进就是逻辑,差一层意思完全不同。
这也是我最终决定做架构重构的直接原因——800 行单文件里的五层嵌套 if/else,太容易出这种错误了。
3.7 跨模型审查系统
为什么不能自己审查自己
最初的审查是用同一个模型来评判自己的输出,结果全部满分。原因是:同一个模型的生成标准和评判标准是同一套权重,它天然觉得自己的输出"不错"。
改成用不同模型做审查(Agent 用 Kimi-k2.5,审查用 Qwen3-max)后,评分更加严格客观。
四次迭代
- 只传回复文字 → 审查模型只听 Agent 自说自话,缺乏判断依据
- 加入工具调用记录 → 审查模型能对照实际数据,提到了具体版本号
- 修复 JSON 解析 → 处理模型返回 markdown 代码块包裹的 JSON
- 加入自动重试 → 审查不通过时将反馈注入 messages,Agent 自动修正
审查的局限性
审查模型给了项目介绍文件 5 分满分,但文件里说这是"基于 Anthropic Claude API 的项目"——实际上我用的是 Kimi。审查模型看到 requirements.txt 里有 anthropic 包就信了。推理型 Sensor 只能基于它看到的信息做判断,有些需要更深层上下文的问题只有人类能发现。
3.8 安全加固:从 eval 到 AST
calculate 函数用的 eval() 是一个严重的安全隐患——虽然有字符白名单,但 eval 的攻击面太大,靠黑名单永远堵不完。
安全领域的原则:不要试图过滤危险输入,而是只允许安全的操作。
用 Python 的 ast 模块把表达式解析成语法树,只允许数字节点和运算符节点通过。任何函数调用、属性访问、列表推导等语法结构都会被拒绝。这是从"字符层面的白名单"升级到"语法层面的白名单"。
有趣的是,Agent 虽然被 calculate_safe 拦住了 __import__('os').listdir('.'),但它"聪明"地绕了一条路——试图写一个 Python 脚本来完成同样的操作。这就是原文说的 Behaviour Harness 是最难的——你堵住了工具层面的漏洞,Agent 在行为层面找到了绕过方式。好在多层防护(写文件需要确认)最终拦住了它。
3.9 架构重构:提升 Harnessability
800 行单文件的痛点很明显:每次改一个小问题都要把整个逻辑看很多次,改到哪里了也不知道。缩进 bug 就是最好的例证。
Harness 原文有一个概念——Harnessability(可驾驭性):不是所有代码库都同样容易加 Harness。好的架构本身就让 Harness 更容易加。
重构后的结构:
my-first-agent/
├── config.py ← 所有配置集中管理
├── main.py ← 入口,20 行
├── agent/
│ ├── __init__.py
│ ├── core.py ← Agent Loop 流程控制
│ ├── tools.py ← 工具实现 + 工具描述
│ ├── security.py ← 权限分级 + 源码保护
│ ├── context.py ← 上下文压缩
│ ├── review.py ← 跨模型审查 + 自动重试
│ └── logger.py ← 日志 + 快照
└── workspace/
重构中的一个关键设计决策:compress_history 和 review_agent_output 从修改全局变量改为接收参数、返回结果。这消除了对全局状态的依赖,也避免了模块之间的循环导入。
SESSION_ID 的归属也引发了思考——它不是配置(不是静态的),跟对话最相关但如果放在 core.py 会导致跟 logger.py 的循环依赖。最终放在 logger.py 里,因为它是日志系统的一部分。
四、完整的 Harness 体系
Guides(前馈控制)
| Guide | 材质 | 作用 |
|---|---|---|
| 工具白名单 | 计算型 | 防止模型幻觉出不存在的工具 |
| 权限分级系统 | 计算型 | 按操作类型 × 路径位置决定是否需要确认 |
| 源码保护 | 计算型 | 禁止修改项目目录下已存在的 .py 文件 |
| 单轮单写限制 | 计算型 | 同一轮响应中只允许一次 write_file |
| AST 安全计算 | 计算型 | 只允许数学运算节点,拒绝一切函数调用和属性访问 |
| System Prompt 规则 | 推理型 | 要求逐文件创建,不一次性完成 |
| Tool Result 停止指令 | 推理型 | 写文件成功后强制模型停下来 |
Sensors(反馈控制)
| Sensor | 材质 | 作用 |
|---|---|---|
| JSONL 日志系统 | 计算型 | 记录每一步决策 |
| Session 快照 | 计算型 | 完整消息历史的持久化 |
| 写入前自动备份 | 计算型 | 确保可逆性 |
| 工具错误捕获 | 计算型 | 返回有意义的错误信息供模型自纠正 |
| 流式输出 | 计算型 | 实时可见性 |
| 跨模型质量审查 | 推理型 | 用 Qwen3-max 审查 Kimi 的输出 |
| 审查驱动自动重试 | 推理型 | 不通过时自动将反馈注入上下文重试 |
Context Engineering 实践
| 技术 | 解决的问题 |
|---|---|
| System Prompt 设计 | Agent 身份和行为规则 |
| 工具描述精确性 | 模型对工具能力的理解 |
| 大文件分段读取 | 防止大文件撑爆上下文 |
| 多格式结构提取 | Python/Markdown/JSON/YAML/SQL/JS 目录 |
| 双重触发压缩 | 条数 + 字节数 |
| 两阶段压缩 | 先截断 tool_result,再 LLM 总结 |
| Tool Result 信息注入 | 把控制指令放在上下文最新位置 |
五、踩坑清单
| 问题 | 根因 | 解决方案 | 分类 |
|---|---|---|---|
| ThinkingBlock 不是文字 | 模型返回格式差异 | 遍历 content 找 text 类型 | 兼容性 |
| SDK 对象无法序列化 | 不是普通字典 | model_dump() 递归转换 | 工程细节 |
| 审查 JSON 解析失败 | 模型返回 markdown 代码块 | 剥掉 ``` 标记 | 推理型 Sensor |
| 审查结果被淹没 | 流式输出 + 主循环重复打印 | 去掉重复打印 | 可观测性 |
| 大文件概览后反复重试 | 返回信息缺乏"成功"标识 | 改措辞 + 改工具描述 | Context Engineering |
| 5 条消息触发 buffer overflow | 只按条数压缩 | 加字节数触发条件 | Context Engineering |
| 缩进导致 result 被覆盖 | if/else 对齐错误 | 修正缩进 + 架构重构 | 工程质量 |
| Agent 绕过工具限制 | 改用写脚本方式 | 多层防护叠加 | Behaviour Harness |
| 模块拆分后循环依赖 | SESSION_ID 归属问题 | 放在 logger.py | 架构设计 |
六、核心认知
6.1 模型是无状态的函数
每次调用都是全新推理。"记忆"是 messages 列表,"工具使用"是结构化文本的生成与解析,"循环"是外部代码的 while True。理解了这一点,才能理解为什么 Context Engineering 如此重要——你喂什么,它就产出什么。
6.2 Harness 是长出来的,不是设计出来的
没有一个 Guide 或 Sensor 是我在第一天就能预见到的。每一个都源自一个真实的失败场景。这就是 Steering Loop 的精髓——发现问题、改进 Harness、继续运行、发现新问题。
6.3 控制强度应与风险成正比
不是所有操作都需要同等控制。计算型控制便宜可靠优先用,推理型控制昂贵但能捕捉语义问题选择性部署。风险越高,控制层数越多。
6.4 好的架构本身就是 Harness
800 行单文件里的缩进 bug 证明了这一点。重构成模块化之后,每个文件的职责清晰,加新的 Guide 和 Sensor 更容易,也更不容易引入 bug。Harnessability 是一个值得从第一天就考虑的属性。
6.5 同一个组件,两个视角
System Prompt 既是 Context Engineering(送进模型的信息)又是 Guide(行为约束)。Tool Result 中的停止指令既是 Context Engineering(注入到上下文)又是 Guide(控制模型行为)。不需要纠结分类,理解它在两个维度上分别起什么作用就够了。
6.6 推理型控制的天花板
同模型审查自信偏高。跨模型审查更客观但仍有盲点——它只能基于看到的信息判断。有些需要深层业务上下文的问题,只有人类能发现。Harness 的目标不是消除人类参与,而是把人类的注意力引导到最需要的地方。
七、与产品级 Agent 的差距
让 Agent 自己分析自己(经人工验证)的结果:
| 维度 | 完成度 | 最关键的缺失 |
|---|---|---|
| 架构设计 | ██████░░░░ 60% | 已完成模块化,但缺少插件化和中间件 |
| 工具生态 | ████░░░░░░ 40% | 缺少代码执行、网络请求、搜索 |
| 安全体系 | ████░░░░░░ 40% | 无沙箱、无路径遍历防护 |
| 智能规划 | ███░░░░░░░ 30% | 缺少多步骤规划、长期记忆 |
| 性能可靠 | ██░░░░░░░░ 25% | 单用户、无限流、无容错 |
| 用户体验 | ██░░░░░░░░ 20% | 缺少多模态、中断恢复 |
| 开发运维 | █░░░░░░░░░ 15% | 无测试、无 CI/CD |
八、最终系统架构
Agent System
│
├── config.py ← 配置中心
├── main.py ← 入口(20 行)
│
├── agent/
│ ├── core.py ← Agent Loop 流程控制
│ │ ├── 流式输出
│ │ ├── tool_use 处理循环
│ │ └── 审查驱动的自纠正循环
│ │
│ ├── tools.py ← 工具层
│ │ ├── calculate(AST 安全版)
│ │ ├── read_file(概览 + 多格式结构提取)
│ │ ├── read_file_lines(按行读取)
│ │ ├── write_file(带备份)
│ │ ├── execute_tool(分发器)
│ │ └── TOOL_DEFINITIONS(工具描述)
│ │
│ ├── security.py ← 安全层
│ │ ├── is_protected_source_file
│ │ ├── needs_confirmation(分级规则)
│ │ └── confirm_tool_call
│ │
│ ├── context.py ← 上下文管理
│ │ ├── estimate_messages_size
│ │ ├── _truncate_tool_result_content
│ │ └── compress_history(双重触发 + 两阶段)
│ │
│ ├── review.py ← 审查系统
│ │ ├── review_agent_output(跨模型)
│ │ ├── should_review_turn(选择性审查)
│ │ ├── build_retry_feedback
│ │ └── get_effective_review_request
│ │
│ └── logger.py ← 可观测性
│ ├── log_event(JSONL)
│ ├── save_session_snapshot
│ └── make_serializable
│
└── 01-05_*.py ← 学习历程记录
九、下一步
- 敏感文件保护:防止 Agent 读取
.env等包含密钥的文件 - Shell 命令执行:风险最高的工具,需要叠加黑名单 + 确认 + 超时 + 日志
- 时序分布:把 Sensor 扩展到 CI/CD 流水线(提交前检查 + 持续漂移检测)
- 工具插件化:让添加新工具变成"注册"而非"修改代码"
十、写在最后
三天时间,从零开始构建了一个有记忆、有工具、有分级控制、有上下文管理、有跨模型审查、有自动纠正的 Agent 系统。过程中踩了无数的坑——缩进 bug、序列化问题、上下文溢出、审查结果被淹没、模型绕过工具限制……
但我最深刻的收获不是这些技术细节,而是一个认知:
Agent 开发不是一次性的设计,而是持续的 Steering Loop。 你写了代码 → 跑出了问题 → 加了一个 Guide 或 Sensor → 又跑出了新问题 → 再迭代。你的 Harness 永远不会"完成",只会越来越健壮。
如果你也想从零开始学 Agent 开发,我的建议是:不要用框架,自己手写 Agent Loop。 只有亲手管理 messages 列表、亲手处理 tool_use 的请求和返回、亲手遇到上下文溢出的问题,你才能真正理解这些框架在帮你做什么。
本文的所有代码均为作者亲手编写和调试。学习过程中与 Claude 协作,采用苏格拉底式教学——Claude 提问引导,作者思考并实现。